﻿//===============================================================================
// Jammer.NET Development Library
// Jmr.Serialization
//===============================================================================
// Copyright © Junaid Fakhruddin, Matthew Leedham and Randolph Cabral.  
// All rights reserved.
//===============================================================================
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================

using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Collections.Generic;

namespace Jmr.Serialization
{
	/// <summary>
	/// BinaryEntityFormatter class.  Formats POCO entity objects into binary stream.
	/// </summary>
	public class BinaryEntityFormatter
	{
		/// <summary>
		/// BinaryEntityFormatter constructor.
		/// </summary>
		public BinaryEntityFormatter() { }

		/// <summary>
		/// Deserializes a stream into an object.
		/// </summary>
		/// <typeparam name="TEntity">Type to deserialize to.</typeparam>
		/// <param name="stream">Stream to deserialize.</param>
		/// <returns>Returns an object.</returns>
		public TEntity Deserialize<TEntity>(Stream stream)
		{
			return (TEntity)Deserialize(typeof(TEntity), stream);
		}

		/// <summary>
		/// Deserializes a stream into an object.
		/// </summary>
		/// <param name="entitytype">Type to deserialize to.</param>
		/// <param name="stream">Stream to deserialize.</param>
		/// <returns>Returns an object.</returns>
		public object Deserialize(Type entitytype, Stream stream)
		{
			var retval = Activator.CreateInstance(entitytype);
			var reader = new BinaryReader(stream);
			JmrSerializationException exception = new JmrSerializationException();

			stream.Seek(0, 0);

			byte objStatus = reader.ReadByte();
			bool isException = (objStatus == 3);
			retval = (isException) ? exception : retval;

			var depth = reader.ReadInt32();

			if (isException)
				retval = DeserializeObject(typeof(JmrSerializationException), stream, depth);
			else
				retval = DeserializeObject(entitytype, stream, depth);

			return retval;
		}

		private object DeserializeObject(Type entitytype, Stream stream, int depth)
		{
			if (depth < 0 || stream.Length == 0) { return null; }

			BinaryReader reader = new BinaryReader(stream);
			
			var retval = default(object);
			if (IsHandledType(entitytype))
				retval = GetHandledTypeInstance(entitytype);
			else
				retval = Activator.CreateInstance(entitytype);

			var length = reader.ReadInt32();
			length += (int)reader.BaseStream.Position;

			byte nullbyte = reader.ReadByte();
			bool isNull = (nullbyte != 2);
			if (isNull) { return null; }

			if (IsHandledType(entitytype))
			{
				retval = ReadValueBytes(entitytype, reader);
				return retval;
			}
			else if (retval is IEnumerable)
			{
				var retvalaslist = ((IList)retval);
				var generictype = entitytype.GetGenericArguments()[0];
				var count = reader.ReadInt32(); //count
				for (int i = 0; i < count; i++)
				{
					retvalaslist.Add(DeserializeObject(generictype, stream, depth));
				}
				return retval;
			}
			else //properties
			{
				while (reader.BaseStream.Position < length)
				{
					var propertyname = reader.ReadString();
					var property = entitytype.GetProperty(propertyname);

					if (!property.CanWrite)
						continue;
					if (property.GetIndexParameters().Count() > 0)
						continue;

					Type propType = (property.PropertyType.Name.ToLower().StartsWith("nullable")) ?
						Nullable.GetUnderlyingType(property.PropertyType) : propType = property.PropertyType;

					nullbyte = reader.ReadByte();
					isNull = (nullbyte != 2);
					if (isNull) { continue; }
					
					var proptypeinstance = default(object);
					if(IsHandledType(propType))
						proptypeinstance = GetHandledTypeInstance(propType);
					else
						proptypeinstance = Activator.CreateInstance(propType);

					if (IsHandledType(propType))
					{
						proptypeinstance = ReadValueBytes(propType, reader);
					}
					else if (proptypeinstance is IEnumerable)
					{
						var retvalaslist = ((IList)proptypeinstance);
						var generictype = propType.GetGenericArguments()[0];
						var count = reader.ReadInt32(); //count
						for (int i = 0; i < count; i++)
						{
							retvalaslist.Add(DeserializeObject(generictype, stream, depth - 1));
						}
					}
					else
						proptypeinstance = DeserializeObject(propType, stream, depth - 1);
					/////

					bool hasvalue = (proptypeinstance != null);
					if (proptypeinstance is string && string.IsNullOrEmpty((string)proptypeinstance))
						hasvalue = false;

					if (property.CanWrite && hasvalue)
					{
						if (propType.Name.ToLower().Contains("guid"))
							property.SetValue(retval, new Guid(proptypeinstance.ToString()), null);
						else
							property.SetValue(retval, Convert.ChangeType(proptypeinstance, propType, CultureInfo.CurrentCulture), null);
					}
				}
				return retval;
			}
		}

		object ReadValueBytes(Type propType, BinaryReader reader)
		{
			var rawvalue = default(object);

			if (propType.Name == typeof(bool).Name)
				rawvalue = reader.ReadBoolean();
			else if (propType.Name == typeof(byte).Name)
				rawvalue = reader.ReadByte();
			else if (propType.Name == typeof(byte[]).Name)
			{
				int bytesLength = reader.ReadInt32();
				rawvalue = (object)reader.ReadBytes(bytesLength);
			}
			else if (propType.Name == typeof(double).Name)
				rawvalue = reader.ReadDouble();
			else if (propType.Name == typeof(float).Name)
				rawvalue = reader.ReadDouble();
			else if (propType.Name == typeof(decimal).Name)
				rawvalue = reader.ReadDouble();
			else if (propType.Name == typeof(int).Name)
				rawvalue = reader.ReadInt32();
			else if (propType.Name == typeof(long).Name)
				rawvalue = reader.ReadInt64();
			else if (propType.Name == typeof(sbyte).Name)
				rawvalue = reader.ReadSByte();
			else if (propType.Name == typeof(short).Name)
				rawvalue = reader.ReadInt16();
			else if (propType.Name == typeof(uint).Name)
				rawvalue = reader.ReadUInt32();
			else if (propType.Name == typeof(ulong).Name)
				rawvalue = reader.ReadUInt64();
			else if (propType.Name == typeof(ushort).Name)
				rawvalue = reader.ReadUInt16();
			else if (propType.Name == typeof(string).Name)
				rawvalue = reader.ReadString();
			else if (propType.Name == typeof(DateTime).Name)
			{
				var year = reader.ReadInt32();//year
				var month = reader.ReadInt32(); //month
				var day = reader.ReadInt32(); //day
				var hour = reader.ReadInt32(); //hour
				var minute = reader.ReadInt32(); //minute
				var second = reader.ReadInt32(); //second
				var millisecond = reader.ReadInt32(); //millisecond
				rawvalue = new DateTime(year, month, day, hour, minute, second, millisecond, CultureInfo.CurrentCulture.Calendar);
			}
			else if (propType.IsEnum)
			{
				rawvalue = Enum.Parse(propType, reader.ReadString(),true);
			}
			else if (propType.Name == typeof(DBNull).Name)
			{
				reader.ReadByte();
				rawvalue = null;
			}
			else if (propType.Name == typeof(System.Object).Name)
			{
				rawvalue = reader.ReadString();
				if (rawvalue == "null")
					rawvalue = null;
			}

			return rawvalue;
		}

		/// <summary>
		/// Serializes an object into a memory stream. (Depth = 0)
		/// </summary>
		/// <param name="entity">Entity to serialize.</param>
		/// <returns>Returns a memory stream.</returns>
		public MemoryStream Serialize(object entity)
		{
			return Serialize(entity, 1);
		}

		/// <summary>
		/// Serializes an object into a memory stream.
		/// </summary>
		/// <param name="entity">Entity to serialize.</param>
		/// <param name="depth">Number of levels to serialize.</param>
		/// <returns>Returns a memory stream.</returns>
		public MemoryStream Serialize(object entity, int depth)
		{
			MemoryStream retVal = new MemoryStream(4096);
			BinaryWriter writer = new BinaryWriter(retVal);
			retVal.Seek(0, 0);

			if (entity is JmrSerializationException)
				WriteExceptionByte(writer);
			else
				WriteSuccessByte(writer);

			WriteDepthByte(depth, writer);
			SerializeObject(retVal, entity, depth);

			return retVal;


			//if (entity is IEnumerable)
			//{
			//	foreach (object obj in (IEnumerable)entity)
			//	{
			//		SerializeObject(retVal, obj, depth);
			//	}
			//}
			//else
			//{
			//	SerializeObject(retVal, entity, depth);
			//}
		}

		private void SerializeObject(Stream stream, object entity, int depth)
		{
			if (depth < 0 || entity == null) { return; }

			MemoryStream writestream = new MemoryStream(4096);
			BinaryWriter writer = new BinaryWriter(writestream);

			var entitytype = entity.GetType();
			WriteNullByte(entity, writer);

			if (entity != null)
			{
				if (IsHandledType(entitytype))
				{
					if (entitytype.Equals(typeof(System.Object)))
						WriteValueBytes(entity.ToString(), writer);
					else
						WriteValueBytes(entity, writer);
				}
				else if (entity is IEnumerable)
				{
					var countProperty = entitytype.GetProperty("Count");
					int count = 0;
					if (countProperty != null)
						count = Convert.ToInt32(entitytype.GetProperty("Count").GetValue(entity, null));

					writer.Write(count); //list count
					foreach (object obj in (IEnumerable)entity)
					{
						SerializeObject(writestream, obj, depth);
					}
				}
				else //property bytes
				{
					var properties = entitytype.FullName.ToLower().Contains("Jmr.Entity.Poco.ListProxy".ToLower())
						? typeof(IList<>).GetProperties() : entity.GetType().GetProperties();

					foreach (PropertyInfo property in properties)
					{
						writer.Write(property.Name);

						if (!property.CanWrite)
							continue;
						if (property.GetIndexParameters().Count() > 0) //skip indexer properties
							continue;

						var rawvalue = entity.GetType().GetProperty(property.Name).GetValue(entity, null);
						var rawvaluetype = (rawvalue == null) ? null : rawvalue.GetType();

						if (WriteNullByte(rawvalue, writer)) { continue; }

						if (IsHandledType(rawvaluetype))
						{
							if (property.PropertyType.Equals(typeof(System.Object)))
								WriteValueBytes(rawvalue.ToString(), writer);
							else
								WriteValueBytes(rawvalue, writer);
						}
						else if (rawvalue is IEnumerable)
						{
							if (depth > 0)
							{
								var countProperty = rawvaluetype.GetProperty("Count");
								int count = 0;
								if (countProperty != null)
									count = Convert.ToInt32(rawvaluetype.GetProperty("Count").GetValue(rawvalue, null));

								writer.Write(count); //list count
								foreach (object obj in (IEnumerable)rawvalue)
								{
									SerializeObject(writestream, obj, depth - 1);
								}
							}
						}
						else
							SerializeObject(writestream, rawvalue, depth - 1);
					}
				}
			}

			MemoryStream memLengthStream = new MemoryStream(4);
			BinaryWriter lengthWriter = new BinaryWriter(memLengthStream);
			int entityStreamLength = (int)writestream.Position;
			lengthWriter.Write(entityStreamLength);
			stream.Write(memLengthStream.GetBuffer(), 0, (int)memLengthStream.Position);
			stream.Write(writestream.GetBuffer(), 0, (int)writestream.Position);
		}

		object GetHandledTypeInstance(Type type)
		{
			object retval = default(object);

			if (type.Equals(typeof(bool)))
				retval = default(bool);
			else if (type.Equals(typeof(byte)))
				retval = default(byte);
			else if (type.Equals(typeof(byte[])))
				retval = default(byte[]);
			else if (type.Equals(typeof(double)))
				retval = default(double);
			else if (type.Equals(typeof(decimal)))
				retval = default(decimal);
			else if (type.Equals(typeof(float)))
				retval = default(float);
			else if (type.Equals(typeof(int)))
				retval = default(int);
			else if (type.Equals(typeof(long)))
				retval = default(long);
			else if (type.Equals(typeof(sbyte)))
				retval = default(sbyte);
			else if (type.Equals(typeof(short)))
				retval = default(short);
			else if (type.Equals(typeof(uint)))
				retval = default(uint);
			else if (type.Equals(typeof(ulong)))
				retval = default(ulong);
			else if (type.Equals(typeof(ushort)))
				retval = default(ushort);
			else if (type.Equals(typeof(string)))
				retval = default(string);
			else if (type.Equals(typeof(DateTime)))
				retval = default(DateTime);
			return retval;
		}

		bool IsHandledType(Type type)
		{
			bool retval = false;

			if (type.Equals(typeof(bool)))
				retval = true;
			else if (type.Equals(typeof(byte)))
				retval = true;
			else if (type.Equals(typeof(byte[])))
				retval = true;
			else if (type.Equals(typeof(double)))
				retval = true;
			else if (type.Equals(typeof(decimal)))
				retval = true;
			else if (type.Equals(typeof(float)))
				retval = true;
			else if (type.Equals(typeof(int)))
				retval = true;
			else if (type.Equals(typeof(long)))
				retval = true;
			else if (type.Equals(typeof(sbyte)))
				retval = true;
			else if (type.Equals(typeof(short)))
				retval = true;
			else if (type.Equals(typeof(uint)))
				retval = true;
			else if (type.Equals(typeof(ulong)))
				retval = true;
			else if (type.Equals(typeof(ushort)))
				retval = true;
			else if (type.Equals(typeof(string)))
				retval = true;
			else if (type.Equals(typeof(DateTime)))
				retval = true;
			else if (type.IsEnum)
				retval = true;
			else if (type.FullName.Contains("System.Object"))
				retval = true;
			else if (type.Equals(typeof(DBNull)))
				retval = true;
			return retval;
		}

		bool WriteValueBytes(object rawvalue, BinaryWriter writer)
		{
			long length = writer.BaseStream.Length;

			if (rawvalue is bool)
				writer.Write((bool)rawvalue);
			else if (rawvalue is byte)
				writer.Write((byte)rawvalue);
			else if (rawvalue is byte[])
			{
				WriteLengthByte(((byte[])rawvalue).Length, writer);
				writer.Write((byte[])rawvalue);
			}
			else if (rawvalue is double)
				writer.Write((double)rawvalue);
			else if (rawvalue is decimal)
			{
				rawvalue = Convert.ChangeType(rawvalue, typeof(double), CultureInfo.CurrentCulture);
				writer.Write((double)rawvalue);
			}
			else if (rawvalue is float)
				writer.Write((float)rawvalue);
			else if (rawvalue is int)
				writer.Write((int)rawvalue);
			else if (rawvalue is long)
				writer.Write((long)rawvalue);
			else if (rawvalue is sbyte)
				writer.Write((sbyte)rawvalue);
			else if (rawvalue is short)
				writer.Write((short)rawvalue);
			else if (rawvalue is uint)
				writer.Write((uint)rawvalue);
			else if (rawvalue is ulong)
				writer.Write((ulong)rawvalue);
			else if (rawvalue is ushort)
				writer.Write((ushort)rawvalue);
			else if (rawvalue is string)
				writer.Write(rawvalue.ToString());
			else if (rawvalue is DateTime && rawvalue != null)
			{
				var value = (DateTime)rawvalue;
				writer.Write(value.Year); //year
				writer.Write(value.Month); //month
				writer.Write(value.Day); //day
				writer.Write(value.Hour); //hour
				writer.Write(value.Minute); //minute
				writer.Write(value.Second); //second
				writer.Write(value.Millisecond); //millisecond
			}
			else if(rawvalue.GetType().IsEnum)
				writer.Write(rawvalue.ToString());

			return (length < writer.BaseStream.Length);
		}
		void WriteExceptionByte(BinaryWriter writer)
		{
			writer.Write((byte)3);
		}

		void WriteSuccessByte(BinaryWriter writer)
		{
			writer.Write((byte)0);
		}

		void WriteLengthByte(int length, BinaryWriter writer)
		{
			writer.Write(length);
		}

		void WriteDepthByte(int depth, BinaryWriter writer)
		{
			writer.Write(depth);
		}

		bool WriteNullByte(object val, BinaryWriter writer)
		{
			if (val == null || val is DBNull)
				writer.Write((byte)1); // if null
			else
				writer.Write((byte)2);

			return (val == null || val is DBNull);
		}
	}

	/// <summary>
	/// JmrSerializationException class.
	/// </summary>
	public class JmrSerializationException
	{
		/// <summary>
		/// ExceptionType member.
		/// </summary>
		public virtual string ExceptionType { get; set; }

		/// <summary>
		/// Message member.
		/// </summary>
		public virtual string Message { get; set; }

		/// <summary>
		/// StackTrace member.
		/// </summary>
		public virtual string StackTrace { get; set; }

		/// <summary>
		/// HelpLink member.
		/// </summary>
		public virtual string HelpLink { get; set; }

		/// <summary>
		/// Source member.
		/// </summary>
		public virtual string Source { get; set; }

		/// <summary>
		/// InnerExceptionType member.
		/// </summary>
		public virtual string InnerExceptionType { get; set; }

		/// <summary>
		/// InnerExceptionMessage member.
		/// </summary>
		public virtual string InnerExceptionMessage { get; set; }

		/// <summary>
		/// InnerExceptionStackTrace member.
		/// </summary>
		public virtual string InnerExceptionStackTrace { get; set; }

		/// <summary>
		/// InnerExceptionHelpLink member.
		/// </summary>
		public virtual string InnerExceptionHelpLink { get; set; }

		/// <summary>
		/// InnerExceptionSource member.
		/// </summary>
		public virtual string InnerExceptionSource { get; set; }

		/// <summary>
		/// Override to return exception source.
		/// </summary>
		/// <returns>Returns a string.</returns>
		public override string ToString()
		{
			return Source;
		}
	}
}
